/*
 * Copyright © 2017 eqxiu.com 北京中网易企秀科技有限公司  All rights reserved.
 */
package cn.hermit.data.xmljson.handler;

import cn.hermit.data.json.handler.JSONHandler;
import cn.hermit.data.json.model.JSONValue;
import cn.hermit.data.xml.model.XmlElement;
import cn.hermit.data.xml.model.XmlNameNode;
import cn.hermit.data.json.JSONChar;
import cn.hermit.data.json.model.JSONArray;
import cn.hermit.data.json.model.JSONDocument;
import cn.hermit.data.json.model.JSONObject;
import cn.hermit.util.StringUtils;
import cn.hermit.data.xml.XmlChar;
import cn.hermit.data.xml.factory.OrderedXmlFactory;
import cn.hermit.data.xml.model.XmlDocument;
import cn.hermit.data.xml.model.XmlFactory;
import cn.hermit.data.xml.ns.NamespaceContext;
import cn.hermit.data.xmljson.XMLJSON;

import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import java.util.Iterator;
import java.util.Map.Entry;

/**
 * Content handler for conversion from JSON to XML.
 * 
 * @author Jack Gao (Chinese name : GAO JIANGUO, Email : linux.gjg@gmail.com)
 * @date 3 Jul, 2014
 */
public class J2XHandler extends JSONHandler {

    /**
     * JSON root key if a JSON object does not have a root key.
     */
    protected String jsonRoot = XMLJSON.DEFAULT_JSON_ROOT;

    /**
     * Indicate if ignore name space definition in JSON in conversion.
     */
    protected boolean ignoredNamespace;

    /**
     * Indicate if ignore name space prefix in JSON in conversion.
     */
    protected boolean ignoredPrefix;

    /**
     * Indicate the JSON key for text node in XML in conversion with non-Badgerfish convention.
     */
    protected String textKey = XMLJSON.DEFAULT_TEXT_JSON_KEY;

    /**
     * Indicate the JSON value for tag-closed XML element.
     */
    protected String closeTagValue = "null";

    /**
     * Indicate if close the tag of element if it is empty.
     */
    protected boolean closeEmptyElement = true;

    /**
     * Indicate if create element for JSON array.
     */
    protected boolean createElementForArray;

    // //////////////////////////////////////

    /**
     * XML factory for creating XML objects.
     */
    protected XmlFactory xmlFactory;

    /**
     * XML Document
     */
    protected XmlDocument xdoc;

    /**
     * Constructor of Content handler for conversion from JSON to XML. Initialized an ordered XML factory.
     */
    public J2XHandler() {
        this(new OrderedXmlFactory());
    }
    /**
     * Constructor of Content handler for conversion from JSON to XML. 
     * 
     * @param xmlFactory
     * 		XML Factory to create XML objects. If it is null, will initialize an ordered XML factory.
     */
    public J2XHandler(XmlFactory xmlFactory) {
        if (xmlFactory == null) {
            xmlFactory = new OrderedXmlFactory();
        }
        this.xmlFactory = xmlFactory;
    }

    /**
     * Invoked when the JSON parser finished walking in JSON document.
     * 
     * <p>
     * In this method, A JSON document has been created. <br/>
     * Then begin to generate a XML document by the JSON document.
     * </p>
     */
    @Override
    public void endDocument() {
        super.endDocument();
        JSONDocument jdoc = super.getDocument();
        refineJSONRoot(jdoc);
        xdoc = this.xmlFactory.createDocument();
        handleXml(null, jdoc, xdoc, -1, new NamespaceContext());
    }

    /**
     * Check if the JSON document has a unique root key.<br/>
     * If there is no unique JSON root key, will append a root with the variable jsonRoot.
     * 
     * @param jdoc
     * 		The JSON document
     */
    private void refineJSONRoot(JSONDocument jdoc) {
        if (StringUtils.isEmpty(this.jsonRoot)) {
            return;
        }
        if (this.jsonRoot.indexOf(':') != -1) {
            return;
        }
        if (!jdoc.isObject()) {
            return;
        }
        JSONObject jo = (JSONObject) jdoc;
        if (jo.size() > 1) {
            Iterator<String> iter = jo.keyIterator();
            while (iter.hasNext()) {
                String key = iter.next();
                if (this.isNamespaceKey(key)) {
                    String prefix = XmlNameNode.getLocalPart(key);
                    if (!StringUtils.isEmpty(prefix)) {
                        this.jsonRoot = prefix + ":" + this.jsonRoot;
                    }
                    return;
                }
            }
        }
    }

    /**
     * Generate XML document recursively according to the JSON docuemnt.
     * 
     * @param key
     *            used for sub arrays
     * @param jdoc
     * 		The JSON document
     * @param elem
     * 		The XML element generated by previous recursive callback
     * @param nsLayer 
     * 		The name space layer in generated XML document tree
     * @param nsContext
     * 		The name space context for generating XML document
     */
    protected void handleXml(String key, JSONDocument jdoc, XmlElement elem, int nsLayer, NamespaceContext nsContext) {
        if (key == null) {
            if (JSONObject.class.isInstance(jdoc)) {
                handleObject(key, (JSONObject) jdoc, elem, nsContext);
            } else if (JSONArray.class.isInstance(jdoc)) {
                handleArray(key, (JSONArray) jdoc, elem, nsContext);
            }
        } else if (isNamespaceKey(key)) {
            if (!this.ignoredNamespace) {
                handleNamespace(key, jdoc, elem, nsLayer, nsContext);
            }
        } else if (isAttributeKey(key)) {
            if (this.ignoredPrefix) {
                key = XmlNameNode.getLocalPart(key);
            }
            handleAttribute(key, jdoc, elem, nsLayer, nsContext);
        } else if (isCDataKey(key)) {
            handleCData(key, jdoc, elem, nsContext);
        } else if (isTextKey(key)) {
            handleText(key, jdoc, elem, nsContext);
        } else {
            if (JSONObject.class.isInstance(jdoc)) {
                handleObject(key, (JSONObject) jdoc, elem, nsContext);
            } else if (JSONArray.class.isInstance(jdoc)) {
                handleArray(key, (JSONArray) jdoc, elem, nsContext);
            } else {
                if (isElementTag(key)) {
                    if (this.ignoredPrefix) {
                        key = XmlNameNode.getLocalPart(key);
                    }
                    String tag = getElementTag(key);
                    QName name = nsContext.getQName(tag);
                    XmlElement subElem = this.xmlFactory.createElement(name);
                    JSONValue jv = (JSONValue) jdoc;
                    if (this.closeTagValue.equals(jv.getValue())) {
                        this.xmlFactory.closeElement(subElem);
                    } else {
                        this.xmlFactory.text(subElem, JSONChar.unescape(
                            jv.getValue().toString(),
                            this.jsonFactory.getValidator().getDefaultValidator().isForwardSlashEscaping()));
                    }
                    this.appendToParent(elem, subElem);
                }
            }
        }
    }

    /**
     * Generate XML object by JSON Array.
     * 
     * @param key
     * 		the key for the JSON Array in parent JSON Object.
     * @param ja
     * 		The JSON Array
     * @param elem
     * 		The XML element generated by previous recursive callback
     * @param nsContext
     * 		The name space context for generating XML document
     */
    protected void handleArray(String key, JSONArray ja, XmlElement elem, NamespaceContext nsContext) {
        if (key != null && this.createElementForArray && isElementTag(key)) {
            if (this.ignoredPrefix) {
                key = XmlNameNode.getLocalPart(key);
            }
            String tag = getElementTag(key);
            QName name = nsContext.getQName(tag);
            XmlElement subElem = this.xmlFactory.createElement(name);
            appendToParent(elem, subElem);
            elem = subElem;
            if (ja.size() == 0) {
                if (this.closeEmptyElement) {
                    this.xmlFactory.closeElement(elem);
                }
                return;
            }
        }
        Iterator<JSONDocument> iter = ja.valueIterator();
        while (iter.hasNext()) {
            handleXml(key, iter.next(), elem, -1, nsContext);
        }
    }

    /**
     * Generate XML object by JSON Object
     * 
     * @param key
     * 		The key of JSON Object in parent JSON Object
     * @param jo
     * 		The JSON Object
     * @param elem
     * 		The XML element generated by previous recursive callback
     * @param nsContext
     * 		The name space context for generating XML document
     */
    protected void handleObject(String key, JSONObject jo, XmlElement elem, NamespaceContext nsContext) {
        int nsLayer = -1;
        if (!StringUtils.isEmpty(key) && isElementTag(key)) {
            if (!this.ignoredNamespace) {
                nsLayer = loadNamespaces(jo, nsContext);
            }
            if (this.ignoredPrefix) {
                key = XmlNameNode.getLocalPart(key);
            }
            String tag = getElementTag(key);
            QName name = nsContext.getQName(tag);
            XmlElement subElem = this.xmlFactory.createElement(name);
            appendToParent(elem, subElem);
            elem = subElem;
            if (jo.size() == 0) {
                if (this.closeEmptyElement) {
                    this.xmlFactory.closeElement(elem);
                }
                return;
            }
        }
        Iterator<Entry<String, JSONDocument>> iter = jo.entryIterator();
        while (iter.hasNext()) {
            Entry<String, JSONDocument> entry = iter.next();
            handleXml(entry.getKey(), entry.getValue(), elem, nsLayer, nsContext);
        }
        if (nsLayer != -1) {
            nsContext.unloadNamespaces(nsLayer);
        }
    }

    /**
     * Generate XML Text node by JSON object.
     * 
     * @param key
     * 		The key of JSON object in parent JSON Object.
     * @param jdoc
     * 		The JSON object
     * @param elem
     * 		The XML element generated by previous recursive callback
     * @param nsContext
     * 		The name space context for generating XML document
     */
    protected void handleText(String key, JSONDocument jdoc, XmlElement elem, NamespaceContext nsContext) {
        if (JSONValue.class.isInstance(jdoc)) {
            JSONValue jv = (JSONValue) jdoc;
            if (this.closeTagValue.equals(jv.getValue())) {
                this.xmlFactory.closeElement(elem);
            } else {
                this.xmlFactory.text(elem, JSONChar.unescape(
                    jv.getValue().toString(),
                    this.jsonFactory.getValidator().getDefaultValidator().isForwardSlashEscaping()));
            }
        } else if (JSONArray.class.isInstance(jdoc)) {
            JSONArray ja = (JSONArray) jdoc;
            Iterator<JSONDocument> iter = ja.valueIterator();
            while (iter.hasNext()) {
                handleText(key, iter.next(), elem, nsContext);
            }
        }
    }

    /**
     * Generate XML CDATA node by JSON object.
     * 
     * @param key
     * 		The key of JSON object in parent JSON Object
     * @param jdoc
     * 		The JSON object
     * @param elem
     * 		The XML element generated by previous recursive callback
     * @param nsContext
     * 		The name space context for generating XML document
     */
    protected void handleCData(String key, JSONDocument jdoc, XmlElement elem, NamespaceContext nsContext) {
        if (JSONValue.class.isInstance(jdoc)) {
            JSONValue jv = (JSONValue) jdoc;
            this.xmlFactory.cdata(elem, JSONChar.unescape(
                jv.getValue().toString(),
                this.jsonFactory.getValidator().getDefaultValidator().isForwardSlashEscaping()));
        }
    }

    /**
     * Generate XML Attribute by JSON object.
     * 
     * @param key
     * 		The key of JSON object in parent JSON Object
     * @param jdoc
     * 		The JSON object
     * @param elem
     * 		The XML element generated by previous recursive callback
     * @param nsLayer
     * 		The name space layer in generated XML document tree
     * @param nsContext
     * 		The name space context for generating XML document
     */
    protected void handleAttribute(
            String key,
            JSONDocument jdoc,
            XmlElement elem,
            int nsLayer,
            NamespaceContext nsContext) {
        if (JSONValue.class.isInstance(jdoc)) {
            JSONValue jv = (JSONValue) jdoc;
            String k = getAttributeKey(key);
            QName name = nsContext.getQName(k);
            appendAttribute(elem, name, jv.getValue().toString());
        }
    }

    /**
     * Append attribute with QName and value to XML Element.
     * 
     * @param elem
     * 		The XML Element
     * @param name
     * 		QName of the attribute
     * @param value
     * 		Value of the attribute
     */
    protected void appendAttribute(XmlElement elem, QName name, String value) {
        if (XmlDocument.class.isInstance(elem)) {
            XmlDocument xdoc = (XmlDocument) elem;
            XmlElement root = xdoc.getRoot();
            if (root == null) {
                QName rootName = XmlNameNode.getName(this.jsonRoot);
                root = this.xmlFactory.createElement(rootName);
                this.xmlFactory.root(xdoc, root);
            }
            elem = root;
        }
        value =
                JSONChar.unescape(value, this.jsonFactory.getValidator().getDefaultValidator().isForwardSlashEscaping());
        this.xmlFactory.attribute(elem, name, value);
    }

    /**
     * Handle name space defined in JSON object for XML Element
     * 
     * @param key
     * 		The key of JSON object in parent JSON Object
     * @param jdoc
     * 		The JSON object
     * @param elem
     * 		The XML element generated by previous recursive callback
     * @param nsLayer
     * 		The name space layer in generated XML document tree
     * @param nsContext
     * 		The name space context for generating XML document
     */
    protected void handleNamespace(
            String key,
            JSONDocument jdoc,
            XmlElement elem,
            int nsLayer,
            NamespaceContext nsContext) {
        if (JSONValue.class.isInstance(jdoc)) {
            JSONValue jv = (JSONValue) jdoc;
            String k = getNamespaceKey(key);
            QName name = nsContext.getQName(k);
            String ns = JSONChar.unescape(jv.getValue().toString(), true);
            ns = ns.replaceAll("\\\\+/", "/");
            appendAttribute(elem, name, ns);
        }
    }

    /**
     * Load name spaces defined in JSON object to name space context.
     * 
     * @param jo
     * 		The JSON object
     * @param nsContext
     * 		The name space context for generating XML document
     */
    protected int loadNamespaces(JSONObject jo, NamespaceContext nsContext) {
        Iterator<Entry<String, JSONDocument>> iter = jo.entryIterator();
        boolean hasIncremented = false;
        while (iter.hasNext()) {
            Entry<String, JSONDocument> entry = iter.next();
            if (JSONValue.class.isInstance(entry.getValue())) {
                JSONValue jv = (JSONValue) entry.getValue();
                if (isNamespaceKey(entry.getKey())) {
                    String ns =
                            XmlChar.validateText(
                                jv.getValue().toString(),
                                this.xmlFactory.getValidator().getDefaultValidator().isReplaceInvalid());
                    ns = JSONChar.unescape(ns, true);
                    ns = ns.replaceAll("\\\\+/", "/");
                    ns = JSONChar.unescape(ns, true);
                    nsContext.loadNamespace(getNamespaceKey(entry.getKey()), ns, !hasIncremented);
                    hasIncremented = true;
                }
            }
        }
        return hasIncremented ? nsContext.getCurrentLayer() : -1;
    }

    /**
     * Append sub XML element to XML Element.
     * 
     * @param elem
     * 		The XML Element
     * @param subElem
     * 		The sub XML element
     */
    protected void appendToParent(XmlElement elem, XmlElement subElem) {
        if (XmlDocument.class.isInstance(elem)) {
            XmlDocument xdoc = (XmlDocument) elem;
            XmlElement root = xdoc.getRoot();
            if (root == null) {
                this.xmlFactory.root(xdoc, subElem);
            } else {
                QName rootName = root.getName();
                QName name = XmlNameNode.getName(this.jsonRoot);
                if (!name.getLocalPart().equals(rootName.getLocalPart())) {
                    XmlElement rootElem = this.xmlFactory.createElement(name);
                    this.xmlFactory.child(rootElem, root);
                    this.xmlFactory.root(xdoc, rootElem);
                    root = rootElem;
                }
                this.xmlFactory.child(root, subElem);
            }
        } else {
            this.xmlFactory.child(elem, subElem);
        }
    }

    /**
     * Check if the JSON key is element tag style
     * @param key
     * 		The JSON key
     * @return
     */
    protected boolean isElementTag(String key) {
        return !StringUtils.isEmpty(key) && !key.startsWith(XMLConstants.XMLNS_ATTRIBUTE) && !key.equals(textKey);
    }

    /**
     * Get element tag from JSON key
     * @param key
     * 		The JSON key
     * @return
     * 		The element tag
     */
    protected String getElementTag(String key) {
        return key;
    }

    /**
     * Check if the JSON key is name space key 
     * @param key
     * 		The JSON key
     * @return
     */
    protected boolean isNamespaceKey(String key) {
        return key.startsWith(XMLConstants.XMLNS_ATTRIBUTE);
    }

    /**
     * Get name space key from a JSON key
     * @param key
     * 		A JSON key
     * @return
     * 		Name space key
     */	
    protected String getNamespaceKey(String key) {
        return key;
    }

    /**
     * Check if the JSON key is attribute key
     * @param key
     * 		The JSON key
     * @return
     */
    protected boolean isAttributeKey(String key) {
        return isNamespaceKey(key);
    }

    /**
     * Get attribute key from a JSON key
     * @param key
     * 		The JSON key
     * @return
     * 		Attribute key
     */
    protected String getAttributeKey(String key) {
        return key;
    }

    /**
     * Check if the JSON key indicates a Text node
     * @param key
     * 		The JSON key
     * @return
     */
    protected boolean isTextKey(String key) {
        return key.equals(textKey);
    }

    /**
     * Check if the JSON key indicates a CATA node
     * @param key
     * 		The JSON key
     * @return
     */
    protected boolean isCDataKey(String key) {
        return false;
    }

    /**
     * Get generated XML document
     * @return
     * 		The generated XML document after conversion
     */
    public XmlDocument getXmlDocument() {
        return xdoc;
    }

    /**
     * Get JSON root
     * 
     * @return 
     * 		the JSON root
     */
    public String getJsonRoot() {
        return jsonRoot;
    }

    /**
     * Set JSON root
     * @param jsonRoot
     *            the JSON root to setHeader
     */
    public void setJsonRoot(String jsonRoot) {
        if (StringUtils.isEmpty(jsonRoot)) {
            jsonRoot = XMLJSON.DEFAULT_JSON_ROOT;
        }
        this.jsonRoot = jsonRoot;
    }

    /**
     * @return the ignoredNamespace
     */
    public boolean isIgnoredNamespace() {
        return ignoredNamespace;
    }

    /**
     * @param ignoredNamespace
     *            the ignoredNamespace to setHeader
     */
    public void setIgnoredNamespace(boolean ignoredNamespace) {
        this.ignoredNamespace = ignoredNamespace;
    }

    /**
     * @return the ignoredPrefix
     */
    public boolean isIgnoredPrefix() {
        return ignoredPrefix;
    }

    /**
     * @param ignoredPrefix
     *            the ignoredPrefix to setHeader
     */
    public void setIgnoredPrefix(boolean ignoredPrefix) {
        ignoredPrefix = ignoredPrefix && ignoredNamespace;
        this.ignoredPrefix = ignoredPrefix;
    }

    /**
     * @return the textKey
     */
    public String getTextKey() {
        return textKey;
    }

    /**
     * @param textKey
     *            the textKey to setHeader
     */
    public void setTextKey(String textKey) {
        this.textKey = textKey;
    }

    /**
     * @return the closeTagValue
     */
    public String getCloseTagValue() {
        return closeTagValue;
    }

    /**
     * @param closeTagValue
     *            the closeTagValue to setHeader
     */
    public void setCloseTagValue(String closeTagValue) {
        this.closeTagValue = closeTagValue;
    }

    /**
     * @return the closeEmptyElement
     */
    public boolean isCloseEmptyElement() {
        return closeEmptyElement;
    }

    /**
     * @param closeEmptyElement
     *            the closeEmptyElement to setHeader
     */
    public void setCloseEmptyElement(boolean closeEmptyElement) {
        this.closeEmptyElement = closeEmptyElement;
    }

    /**
     * @return the createElementForArray
     */
    public boolean isCreateElementForArray() {
        return createElementForArray;
    }

    /**
     * @param createElementForArray
     *            the createElementForArray to setHeader
     */
    public void setCreateElementForArray(boolean createElementForArray) {
        this.createElementForArray = createElementForArray;
    }

    /**
     * Get XML factory to generate XML objects
     * 
     * @return The XML factory
     */
    public XmlFactory getXmlFactory() {
        return xmlFactory;
    }

}